package ph.com.gs3.formalistics.model.dao;

import java.util.ArrayList;
import java.util.List;

import ph.com.gs3.formalistics.global.utilities.Serializer;
import ph.com.gs3.formalistics.global.utilities.logging.FLLogger;
import ph.com.gs3.formalistics.global.utilities.logging.FLLogger.LogType;
import ph.com.gs3.formalistics.model.DatabaseHelper;
import ph.com.gs3.formalistics.model.tables.UserAccountTable;
import ph.com.gs3.formalistics.model.valueobjects.business.User;
import ph.com.gs3.formalistics.model.valueobjects.business.User.ACTIVITY;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;

public class UserAccountsDAO extends SQLiteDataAccessObject {

	public static final String TAG = UserAccountsDAO.class.getSimpleName();

	private String[] tableColumns;

	public static enum PasswordExclusion {
		EXCLUDE_PASSWORD, INCLUDE_PASSWORD
	}

	public static enum UpdateField {
		ALL_FORMS, ALL_DOCUMENTS, ALL_COMMENTS
	}

	public UserAccountsDAO(DatabaseHelper databaseHelper) {
		super(databaseHelper);
	}

	public UserAccountsDAO(Context context) {
		super(context);

		tableColumns = new UserAccountTable().getTableColumns();
	}

	// ============================================================================
	// {{ Update & Insert Methods

	/**
	 * Checks if a user is already existing in the database, if he is, then that user is
	 * updated, otherwise, a new user is created in the database.
	 * 
	 * @param user
	 *            the user object to be saved.
	 * @return the updated user object, this contains the new generated id if a new user
	 *         is created.
	 * @throws IncompleteUserValuesException
	 */
	public User saveOrUpdateUser(User user) throws IncompleteUserValuesException {

		validateUserForRegistration(user);

		User existingUser = getUserWithWebIdAndServer(user.getWebId(), user.getAccountServer());
		User resultingUser = null;

		// register or update user

		if (existingUser != null) {
			int affectedRows = updateUser(user, user.getWebId(), user.getAccountServer());

			if (affectedRows > 0) {
				resultingUser = getUserWithWebIdAndServer(user.getWebId(), user.getAccountServer());
			} else {
				// TODO: throw exception here
				resultingUser = existingUser;
			}
		} else {
			resultingUser = insertUser(user);
		}

		return resultingUser;

	}

	private User insertUser(User user) {

		ContentValues cv = createCVFromUser(user);

		int insertId = (int) database.insert(UserAccountTable.NAME, null, cv);

		// log any possible errors
		if (insertId == -1) {	// if no data is inserted
			FLLogger.log(
			        TAG,
			        LogType.ERROR,
			        "No user data was inserted to database upon registration. User data is: "
			                + cv.toString());
		}

		return getUserWithId(insertId);

	}

	public int updateUser(User newUser, int webId, String server,
	        PasswordExclusion passwordExclusion) {

		ContentValues cv = createCVFromUser(newUser);

		if (passwordExclusion == PasswordExclusion.EXCLUDE_PASSWORD) {
			cv.remove(UserAccountTable.COL_PASSWORD);
		}

		//	@formatter:off
		String whereClause 	= UserAccountTable.COL_WEB_ID + " = ? AND "
			        		+ UserAccountTable.COL_SERVER + " = ?";
		String[] whereArgs = { Integer.toString(webId), server };
			
		int affectedRows = database.update(UserAccountTable.NAME, cv, whereClause, whereArgs);
			
		//	@formatter:on

		return affectedRows;

	}

	public int updateUser(User newUser, int webId, String server) {
		return updateUser(newUser, webId, server, PasswordExclusion.INCLUDE_PASSWORD);
	}

	public void setUserLastUpdateField(String server, int webId, String lastUpdateDateString,
	        UpdateField updateField) throws InsertFailedException {

		// determine the column to update
		String updateColumnName;

		switch (updateField) {
			case ALL_FORMS: {
				updateColumnName = UserAccountTable.COL_LAST_ALL_FORMS_UPDATE;
			}
				break;
			case ALL_DOCUMENTS: {
				updateColumnName = UserAccountTable.COL_LAST_ALL_DOCUMENTS_UPDATE;

			}
				break;
			case ALL_COMMENTS: {
				updateColumnName = UserAccountTable.COL_LAST_ALL_COMMENTS_UPDATE;
			}
				break;
			default: {
				throw new IllegalArgumentException("Update field cannot be determined");
			}
		}

		// update the record
		ContentValues cv = new ContentValues();
		cv.put(updateColumnName, lastUpdateDateString);

		// @formatter:off
		String whereClause 	= UserAccountTable.COL_WEB_ID + " = ? AND "
				        	+ UserAccountTable.COL_SERVER + " = ?";
		String[] whereArgs 	= { Integer.toString(webId), server };

		int affectedRows = database.update(UserAccountTable.NAME, cv, whereClause, whereArgs);

		// @formatter:on

		if (affectedRows <= 0) {
			throw new InsertFailedException();
		}

		if (affectedRows > 1) {
			FLLogger.w(TAG, "There are more than one records affected by setting the "
			        + updateColumnName + " .");
		}

	}

	// shortcut methods
	public void setLastAllFormsUpdate(String server, int webId, String lastFormsUpdateString)
	        throws InsertFailedException {

		setUserLastUpdateField(server, webId, lastFormsUpdateString, UpdateField.ALL_FORMS);
	}

	// shortcut methods
	public void setLastAllDocumentsUpdate(String server, int webId, String lastDocumentsUpdateString)
	        throws InsertFailedException {

		setUserLastUpdateField(server, webId, lastDocumentsUpdateString, UpdateField.ALL_DOCUMENTS);
	}

	// shortcut methods
	public void setLastAllCommentsUpdate(String server, int webId, String lastCommentsUpdateString)
	        throws InsertFailedException {

		setUserLastUpdateField(server, webId, lastCommentsUpdateString, UpdateField.ALL_COMMENTS);
	}

	/**
	 * Marks the specified user as active in the database and deactivates all other users.
	 * 
	 * @param user
	 * @return
	 * @throws UserNotFoundException
	 */
	public void activateUser(int userId) throws UserNotFoundException {

		ContentValues activeUsersCV = new ContentValues();
		ContentValues inactiveUsersCV = new ContentValues();

		activeUsersCV.put(UserAccountTable.COL_IS_ACTIVE, ACTIVITY.ACTIVE);
		inactiveUsersCV.put(UserAccountTable.COL_IS_ACTIVE, ACTIVITY.INACTIVE);

		String whereClause = UserAccountTable.COL_ID + " = ?";
		String[] whereArgs = { Integer.toString(userId) };

		int affectedRows = database.update(UserAccountTable.NAME, activeUsersCV, whereClause,
		        whereArgs);

		if (affectedRows <= 0) {
			// unable to update anything, the record of the user is not found.
			throw new UserNotFoundException("Failed to activate user with id = " + userId
			        + ", the specified user is not found.");
		} else {
			// deactivate remaining users
			String deactivateWhereClause = UserAccountTable.COL_ID + " != ?";
			database.update(UserAccountTable.NAME, inactiveUsersCV, deactivateWhereClause,
			        whereArgs);
		}

	}

	public void deactivateAllUsers() {

		ContentValues cv = new ContentValues();

		cv.put(UserAccountTable.COL_IS_ACTIVE, ACTIVITY.INACTIVE);

		int affectedRows = database.update(UserAccountTable.NAME, cv, null, null);
		if (affectedRows <= 0) {
			FLLogger.w(TAG, "Warning, no user deactivated");
		}
	}

	// }}

	// ============================================================================
	// Query Methods

	/**
	 * Gets the currently active user account saved in this device. If no such use is
	 * found, null is returned.
	 * 
	 * @return a User object containing the data of the active user if it's found,
	 *         otherwise, null.
	 */
	public User getActiveUserAccount() {

		String whereClause = UserAccountTable.COL_IS_ACTIVE + " = ?";
		String[] whereArgs = new String[] { Integer.toString(User.ACTIVITY.ACTIVE) };
		Cursor cursor = database.query(UserAccountTable.NAME, tableColumns, whereClause, whereArgs,
		        null, null, null);

		if (cursor.moveToFirst() && cursor.getCount() > 0) {
			User user = cursorToUser(cursor);
			cursor.close();

			return user;
		} else {
			FLLogger.w(TAG, "No active user account");
			return null;
		}
	}

	/**
	 * Gets a user by its id.
	 * 
	 * @param id
	 *            the id of the user to get.
	 * @return a User object containing the data of the active user if it's found,
	 *         otherwise, null.
	 */
	public User getUserWithId(int id) {

		String whereClause = UserAccountTable.COL_ID + " = " + id;
		Cursor cursor = database.query(UserAccountTable.NAME, tableColumns, whereClause, null,
		        null, null, null);

		if (cursor.moveToFirst() && cursor.getCount() > 0) {
			User user = cursorToUser(cursor);
			cursor.close();

			return user;
		} else {
			return null;
		}
	}

	/**
	 * 
	 * Gets a user by its web(formalistics) id and server.
	 * 
	 * @param webId
	 *            the web id of the user to get
	 * @param server
	 *            the server of the user to get.
	 * @return a User object containing the data of the active user if it's found,
	 *         otherwise, null.
	 */
	public User getUserWithWebIdAndServer(int webId, String server) {

		//	@formatter:off
		String selection = UserAccountTable.COL_WEB_ID + " = ? AND " + UserAccountTable.COL_SERVER + " = ?";
		String selectionArgs[] = { Integer.toString(webId), server };
		
		Cursor cursor = database.query(
				UserAccountTable.NAME, tableColumns, selection, selectionArgs, 
			    null, null, null);
		
		//	@formatter:on

		boolean userExists = (cursor.moveToFirst() && cursor.getCount() > 0);

		User user = null;

		if (userExists) {
			user = cursorToUser(cursor);
			cursor.close();
		}

		return user;

	}

	public List<String> getAvailableUserIdListByServer(String server) {

		String selection = UserAccountTable.COL_SERVER + "=?";
		String[] selectionArgs = { server };

		String[] selecUsertionColumns = { UserAccountTable.COL_WEB_ID };
		Cursor cursor = database.query(UserAccountTable.NAME, selecUsertionColumns, selection,
		        selectionArgs, null, null, null);

		cursor.moveToFirst();

		List<String> userIdList = new ArrayList<>();

		int columnIndex = cursor.getColumnIndexOrThrow(UserAccountTable.COL_WEB_ID);

		while (!cursor.isAfterLast()) {
			userIdList.add(cursor.getString(columnIndex));
			cursor.moveToNext();
		}

		return userIdList;

	}

	// ============================================================================
	// {{ Validation Methods

	public void validateUserForRegistration(User user) throws IncompleteUserValuesException {

		List<String> missedFields = new ArrayList<String>();

		if (user.getWebId() <= 0) {
			missedFields.add("Web ID");
		}

		if (user.getAccountServer() == null || user.getAccountServer().isEmpty()) {
			missedFields.add("Server");
		}

		if (missedFields.size() > 0) {
			throw new IncompleteUserValuesException(missedFields);
		}

	}

	// }}

	// ============================================================================
	// {{ Exceptions

	public static class IncompleteUserValuesException extends Exception {

		private static final long serialVersionUID = -1687590760970743171L;

		public IncompleteUserValuesException(String[] values) {
			super("The following values must not be blank or null: "
			        + Serializer.serializeArray(values));
		}

		public IncompleteUserValuesException(List<String> values) {
			super("The following values must not be blank or null: "
			        + Serializer.serializeList(values));
		}

	}

	public static class UserNotFoundException extends Exception {

		private static final long serialVersionUID = -3699845269971272053L;

		public UserNotFoundException(String message) {
			super(message);
		}

	}

	// }}

	// ============================================================================
	// {{ Parser Methods

	/**
	 * Creates a new ContentValues object based on the passed user object. The keys are
	 * based on the columns of the User_Accounts table. The created ContentValues object
	 * does not contain the id of the user object, only the web_id so that the
	 * ContentValues object can be ready for insert.
	 * 
	 * @param user
	 *            the user object where the new ContentValues object will be based on.
	 * @return
	 */
	public ContentValues createCVFromUser(User user) {

		ContentValues cv = new ContentValues();

		// the id is intentionally not set, it's auto generated

		cv.put(UserAccountTable.COL_COMPANY_ID, user.getCompanyId());
		cv.put(UserAccountTable.COL_COMPANY_NAME, user.getCompanyName());

		cv.put(UserAccountTable.COL_WEB_ID, user.getWebId());
		cv.put(UserAccountTable.COL_EMAIL, user.getEmail());
		cv.put(UserAccountTable.COL_DISPLAY_NAME, user.getDisplayName());
		cv.put(UserAccountTable.COL_IMAGE_URL, user.getImageURL());

		// TODO: add an encryption task here
		cv.put(UserAccountTable.COL_PASSWORD, user.getPassword());

		cv.put(UserAccountTable.COL_SERVER, user.getAccountServer());
		cv.put(UserAccountTable.COL_USER_LEVEL_ID, user.getUserLevel());
		cv.put(UserAccountTable.COL_IS_ACTIVE, user.getActivity());

		// the update fields are intentionally not set

		return cv;

	}

	private User cursorToUser(Cursor cursor) {

		User user = new User();

		//	@formatter:off
		//	initialize indices		
		int idIndex 			= cursor.getColumnIndexOrThrow(UserAccountTable.COL_ID);
		
		int companyIdIndex		= cursor.getColumnIndexOrThrow(UserAccountTable.COL_COMPANY_ID);
		int companyNameIndex	= cursor.getColumnIndexOrThrow(UserAccountTable.COL_COMPANY_NAME);
		
		int webIdIndex			= cursor.getColumnIndexOrThrow(UserAccountTable.COL_WEB_ID);
		int emailIndex			= cursor.getColumnIndexOrThrow(UserAccountTable.COL_EMAIL);
		int displayNameIndex	= cursor.getColumnIndexOrThrow(UserAccountTable.COL_DISPLAY_NAME);
		int imageURLIndex		= cursor.getColumnIndexOrThrow(UserAccountTable.COL_IMAGE_URL);
		
		int passwordIndex		= cursor.getColumnIndexOrThrow(UserAccountTable.COL_PASSWORD);
		int serverIndex			= cursor.getColumnIndexOrThrow(UserAccountTable.COL_SERVER);
		int userLevelIndex		= cursor.getColumnIndexOrThrow(UserAccountTable.COL_USER_LEVEL_ID);				

		int lastFormsUpdateIndex	= cursor.getColumnIndexOrThrow(UserAccountTable.COL_LAST_ALL_FORMS_UPDATE);
		int lastDocumentsUpdateIndex	= cursor.getColumnIndexOrThrow(UserAccountTable.COL_LAST_ALL_FORMS_UPDATE);
		int lastCommentsUpdateIndex	= cursor.getColumnIndexOrThrow(UserAccountTable.COL_LAST_ALL_FORMS_UPDATE);		
		
		//	initialize values
		int id				= cursor.getInt(idIndex);		
		int webId			= cursor.getInt(webIdIndex);
		
		String companyId	= cursor.getString(companyIdIndex);
		String companyName	= cursor.getString(companyNameIndex);
		
		String email		= cursor.getString(emailIndex);
		String displayName 	= cursor.getString(displayNameIndex);
		String imageURL		= cursor.getString(imageURLIndex);
		
		String password		= cursor.getString(passwordIndex);
		String server		= cursor.getString(serverIndex);
		int userLevel		= cursor.getInt(userLevelIndex);
		
		String lastFormsUpdate	 	= cursor.getString(lastFormsUpdateIndex);
		String lastDocumentsUpdate	= cursor.getString(lastDocumentsUpdateIndex);
		String lastCommentsUpdate	= cursor.getString(lastCommentsUpdateIndex);
		
		//	@formatter:on

		// set the user values
		user.setId(id);
		user.setWebId(webId);

		user.setCompanyId(companyId);
		user.setCompanyName(companyName);

		user.setEmail(email);
		user.setDisplayName(displayName);
		user.setImageURL(imageURL);

		user.setPassword(password);
		user.setAccountServer(server);
		user.setUserLevel(userLevel);

		user.setLastAllFormsUpdate(lastFormsUpdate);
		user.setLastAllDocumentsUpdate(lastDocumentsUpdate);
		user.setLastAllCommentsUpdate(lastCommentsUpdate);

		return user;

	}

	// }}

}
